#! -*- coding: utf-8 -*-

import logging
from flask import current_app

from pyelasticsearch import ElasticSearch
from app.utils import mars_to_earth, filter_text
# from data_to_doc import poi_to_doc

log_debug = logging.getLogger("stdout")
log_error = logging.getLogger('sentry_logger')
sys_logger = logging.getLogger('btsyslog.search_log')

SEARCHD_OK = 0  # 搜索状态为正常
SEARCHD_ERROR = 1  # 搜索异常

import socket
import types
import ujson
import jieba
import json
from struct import unpack


class Promise(object):
    """
    This is just a base class for the proxy class created in
    the closure of the lazy function. It can be used to recognize
    promises in code.
    """
    pass


def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
    """
    Returns a bytestring version of 's', encoded as specified in 'encoding'.

    If strings_only is True, don't convert (some) non-string-like objects.
    """
    if strings_only and isinstance(s, (types.NoneType, int)):
        return s
    if isinstance(s, Promise):
        return unicode(s).encode(encoding, errors)
    elif not isinstance(s, basestring):
        try:
            return str(s)
        except UnicodeEncodeError:
            if isinstance(s, Exception):
                # An Exception subclass containing non-ASCII data that doesn't
                # know how to print itself properly. We shouldn't raise a
                # further exception.
                return ' '.join([smart_str(arg, encoding, strings_only,
                                           errors) for arg in s])
            return unicode(s).encode(encoding, errors)
    elif isinstance(s, unicode):
        return s.encode(encoding, errors)
    elif s and encoding != 'utf-8':
        return s.decode('utf-8', errors).encode(encoding, errors)
    else:
        return s


class SegTxt(object):
    """
    结巴分词客户端
    ===============
    search:  搜索模式
    full: 全模式
    default: 精确模式
    """

    def __init__(self):
        self.HOST = current_app.config['TCPSERVER_HOST']
        self.PORT = 8888

    def search(self, txt):
        return self._cut(smart_str(txt), method='search')

    def full(self, txt):
        return self._cut(smart_str(txt), method='full')

    def default(self, txt):
        return self._cut(smart_str(txt), method='default')

    def _cut(self, txt, method=None):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.HOST, self.PORT))
        if method:
            value = "%s|%s&&" % (method, txt)
        else:
            value = "%s|%s&&" % ("default", txt)
        try:
            sock.send(value)
            sock.settimeout(5)
            (status, ver, length) = unpack('>2HL', sock.recv(8))
            if status != 0:
                return []
            response = ''
            left = length
            if left <= 0:
                return left
            while left > 0:
                chunk = sock.recv(left)
                if chunk:
                    response += chunk
                    left -= len(chunk)
                else:
                    break
            if not response or len(response) != length:
                if length:
                    self._error = 'failed to read searchd response (status=%s, ver=%s, len=%s, read=%s)' \
                                  % (status, ver, length, response)
                else:
                    self._error = 'received zero-sized searchd response'
                return []
            data = json.loads(response)
        except:
            data = []
        sock.close()
        return data


class SearchClient(object):
    def __init__(self, evn=""):
        self._error = ""
        self.es_client = ElasticSearch(current_app.config['EL_ADDRESS'])
        self.source_mapping = {
            "user": self.__query_user,
            "comment": self.__query_comment,
            "poi": self.__query_poi,
            "trip": self.__query_trip,
            "place": self.__query_place,
            "waypoint": self.__query_waypoint,
        }
        self.evn = evn

        self.user_index_name = "users_prod" if self.evn == "prod" else "users"
        self.user_index_doc_type = "user"
        self.comment_index_name = "comments_prod" if self.evn == "prod" else "comments"
        self.comment_index_doc_type = "comment"
        self.poi_index_name = "pois_prod" if self.evn == "prod" else "pois"
        self.poi_index_doc_type = "poi"
        self.trip_index_name = "trips_prod" if self.evn == "prod" else "trips"
        self.trip_index_doc_type = "trip"
        self.place_index_name = "places_prod" if self.evn == "prod" else "places"
        self.place_index_doc_type = "place"
        self.vacation_index_name = "vacation_pois_prod" if self.evn == "prod" else "vacation_pois"
        self.vacation_index_doc_type = "poi"
        self.vacation_mtu_index_name = "vacation_mtus_prod" if self.evn == "prod" else "vacation_mtus"
        self.vacation_mtu_index_doc_type = "mtu"
        self.trip_product_index_name = "trip_products_prod" if self.evn == "prod" else "trip_products"
        self.trip_product_doc_type = "trip_product"
        self.waypoint_index_name = "waypoints_prod" if self.evn == "prod" else "waypoints"
        self.waypoint_index_doc_type = "waypoint"

    def process_list_data(self, item):
        i = item[0] if isinstance(item, list) else item
        return i

    def query(self, keyword="", source="user", offset=0, limit=20, **kwargs):
        """
            keyword @要搜索的字符串
            source   @要搜索的资源 user=用户
            offset
            limit
            kwargs @ 搜索poi 支持  <category  lat  lon place_id vacation_option(True or False)>
            return 搜索返回结果
        """
        if not self.source_mapping.get(source):
            return None
        if source != "comment" and keyword:
            cut_client = SegTxt()
            keyword = cut_client.default(filter_text(keyword))
            keyword = " ".join(keyword)
        keyword = keyword.lower() if keyword else keyword
        if offset < 0:
            offset = 0
        if limit < 0:
            limit = 0
        result = self.source_mapping.get(source)(keyword, offset, limit, **kwargs)
        result['words'] = keyword
        if keyword:
            log_data = '%s|%s' % (source, ujson.encode(result))
            sys_logger.info(log_data)
        return result

    def __query_place(self, name, offset=0, limit=20, **kwargs):
        """
            检索地点
        """
        q = "name:%s" % name
        query_string = {
            "fields": ["name", "p_id", "type"],
            "from": offset, "size": limit,
            "query": {
                "query_string": {
                    "query": q,
                    "default_operator": "AND"
                }

            }
        }
        sort = [
            {"popularity": "desc"},
            {
                "_score": {
                    "order": "desc",
                },

            },
        ]
        query_string["sort"] = sort
        countrys = []
        citys = []
        provinces = []
        result = dict(code=SEARCHD_OK, total=0)
        result["citys"] = citys
        result['countrys'] = countrys
        result['provinces'] = provinces
        if not name:
            return result
        try:
            r = self.es_client.search(query_string, index=self.place_index_name, doc_type=self.place_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.exception(e)
            result['code'] = SEARCHD_ERROR
        else:
            hits = r.get("hits")
            total = hits.get("total")
            for p in hits.get("hits"):
                fields = p.get("fields")
                p_type = int(self.process_list_data(fields.get("type")))
                p_id = int(self.process_list_data(fields.get("p_id")))
                log_debug.debug(fields.get("name"))

                if p_type == 1:
                    countrys.append(p_id)
                elif p_type == 2:
                    provinces.append(p_id)
                elif p_type == 3:
                    citys.append(p_id)

            result["citys"] = citys
            result['countrys'] = countrys
            result['provinces'] = provinces
            result['total'] = total
        return result

    def __query_user(self, user_name, offset=0, limit=20, **kwargs):
        """
            检索用户
        """
        result = dict(code=SEARCHD_OK, total=0, uids=[])
        if not user_name:
            return result
        try:
            q = "user_name:%s" % user_name

            query_string = {
                "fields": ["user_id"],
                "from": offset, "size": limit,
                "query": {
                    "query_string": {
                        "query": q,
                        "default_operator": "AND"
                    }
                }
            }
            r = self.es_client.search(query_string, index=self.user_index_name, doc_type=self.user_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.exception(e)
            result['code'] = SEARCHD_ERROR
        else:
            hits = r.get("hits")
            total = hits.get("total")
            uids = [int(u.get("_id")) for u in hits.get("hits")]
            result['total'] = total
            uids = [self.process_list_data(_) for _ in uids]
            result['uids'] = uids
        return result

    def __query_comment(self, text, offset=0, limit=20, **kwargs):
        """
            检索评论
        """

        q = "comment:%s" % text

        query_string = {
            "fields": ["c_id"],
            "from": offset, "size": limit,
            "query": {
                "query_string": {
                    "query": q
                }
            }
        }
        result = dict(code=SEARCHD_OK, total=0, c_ids=[])
        try:
            r = self.es_client.search(query_string, index=self.comment_index_name, doc_type=self.comment_index_doc_type)
        except Exception, e:
            log_error.exception(e)
            log_debug.error(e, exc_info=True)
            result['code'] = SEARCHD_ERROR
        else:
            hits = r.get("hits")
            total = hits.get("total")
            c_ids = [u.get("fields").get("c_id") for u in hits.get("hits")]
            c_ids = [self.process_list_data(_) for _ in c_ids]
            result['total'] = total
            result['c_ids'] = c_ids
        return result

    def __generate_query_poi_string(self, keyword, category, lat, lon, city_id, province_id, country_id,
                                    vacation_option=False, has_cover=False, recommended=False):
        if not lat or not lon:
            search_key = "desc"
        else:
            search_key = "name"
        q = list()
        if keyword:
            q.append("%s:%s" % (search_key, keyword))
        if category:
            q.append(" category_id:%d" % int(category))
        if country_id:
            q.append(" country_id:%d" % int(country_id))
        if province_id:
            q.append(" province_id:%d" % int(province_id))
        if city_id:
            q.append(" city_id:%d" % int(city_id))
        if vacation_option:
            q.append(" vacation_option:1")
        else:
            q.append(" vacation_only:0")
        if has_cover:
            q.append(" has_cover:1")
        if recommended:
            q.append(" recommended:1")

        if q:
            query_string = {
                "query_string": {
                    "query": "".join(q),
                    "default_operator": "AND"
                }
            }
        else:
            query_string = {
                "match_all": {}
            }
        return query_string

    def update_waypoints_2es(self, docs):
        update_docs = []
        delete_docs = []
        for doc in docs:
            is_active = doc.pop('is_active')
            waypoint_id = doc['waypoint_id']

            text = doc['text']
            trip_name = doc.pop('trip_name')

            text = list(jieba.cut_for_search(filter_text(text)))
            trip_name = jieba.cut_for_search(filter_text(trip_name))
            text.extend(trip_name)
            text = " ".join(text)
            doc['text'] = text

            if is_active:
                update_docs.append(doc)
            else:
                delete_docs.append(str(waypoint_id))

        # bulk update docs
        base_kwargs = dict(index=self.waypoint_index_name, doc_type=self.waypoint_index_doc_type)
        if update_docs:
            self.es_client.bulk_index(docs=update_docs, id_field='waypoint_id', **base_kwargs)

        # bulk delete docs
        if delete_docs:
            q = {
                "query": {
                    "ids": {
                        "type": "waypoint",
                        "values": delete_docs
                    }
                }
            }
            self.es_client.delete_by_query(query=q, **base_kwargs)

        # refresh index
        self.es_client.refresh(self.waypoint_index_name)

    def __query_waypoint(self, text, offset=0, limit=20, **kwargs):
        """
            检索waypoint
        """

        q = []
        if text:
            q.append("text:%s" % text)

        if "fdfs_photo" in kwargs and kwargs['fdfs_photo'] != "":
            q.append("fdfs_photo:%s" % (1 if kwargs["fdfs_photo"] else 0))

        if "qiniu_photo" in kwargs and kwargs['qiniu_photo'] != "":
            q.append("qiniu_photo:%s" % (1 if kwargs["qiniu_photo"] else 0))

        if "photo" in kwargs and kwargs['photo'] != "":
            q.append("photo:%s" % (1 if kwargs["photo"] else 0))

        if 'spam_level' in kwargs and kwargs['spam_level']:
            q.append("spam_level:%s" % kwargs['spam_level'])

        q = " ".join(q)
        query_string = {
            "fields": ["waypoint_id"],
            "from": offset, "size": limit,
            "query": {
                "query_string": {
                    "query": q,
                    "default_operator": "and"
                }
            }
        }

        sort_key = kwargs.pop("sort", "last_modified")  # _score
        if sort_key != "last_modified":
            sort_key = "_score"

        query_string['sort'] = [{sort_key: "desc"}]

        result = dict(code=SEARCHD_OK, total=0, waypoint_ids=[])
        try:
            r = self.es_client.search(query_string, index=self.waypoint_index_name,
                                      doc_type=self.waypoint_index_doc_type)
        except Exception, e:
            log_error.exception(e)
            log_debug.error(e, exc_info=True)
            result['code'] = SEARCHD_ERROR
        else:
            hits = r.get("hits")
            total = hits.get("total")
            waypoint_ids = [u.get("fields").get("waypoint_id") for u in hits.get("hits")]
            waypoint_ids = [self.process_list_data(_) for _ in waypoint_ids]
            result['total'] = total
            result['waypoint_ids'] = waypoint_ids
        return result

    def __query_poi(self, keyword="", offset=0, limit=20, **kwargs):
        category = kwargs.get("category", 0)
        lat = kwargs.get("lat", 0.0)
        lon = kwargs.get("lon", 0.0)
        country_id = kwargs.get("country_id", 0)
        province_id = kwargs.get("province_id", 0)
        city_id = kwargs.get("city_id", 0)
        vacation_option = kwargs.get("vacation_option", False)
        recommended = kwargs.get("recommended", False)
        query_string = self.__generate_query_poi_string(keyword, category, lat, lon, city_id, province_id, country_id,
                                                        vacation_option, recommended=recommended)
        query = dict(query=query_string)
        if lat and lon:
            lat, lon = mars_to_earth((lat, lon))
            sort = [
                {
                    "_geo_distance": {
                        "location": {
                            "lat": lat,
                            "lon": lon
                        },
                        "order": "asc",
                        "unit": "km"
                    },

                },
                {
                    "_score": {
                        "order": "desc",
                    },

                },
            ]
        else:
            sort = [
                {"recommended": "desc"},
                {"popularity": "desc"},
                {
                    "_score": {
                        "order": "desc",
                    },
                },
            ]
        if sort:
            query['sort'] = sort
        el_from = {
            "from": offset,
            "size": limit
        }
        query['from'] = el_from
        result = dict(code=SEARCHD_OK)
        result['distance'] = {}
        result['poi_ids'] = []
        try:
            log_debug.debug(query)
            res = self.es_client.search(query, index=self.poi_index_name, doc_type=self.poi_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.error(e, exc_info=True)
            result['code'] = SEARCHD_ERROR
        else:
            hits = res.get("hits")
            log_debug.debug(hits.get("total", 0))
            total = hits.get("total")
            poi_ids = list()
            distance = dict()
            for poi in hits.get("hits"):
                poi_id = int(poi.get("_id"))
                poi_ids.append(poi_id)
                _sort = poi.get("sort")
                if _sort and (lat and lon):
                    distance[poi_id] = poi.get("sort")[0]

            log_debug.debug(poi_ids)
            result['total'] = total
            result['distance'] = distance
            result['poi_ids'] = poi_ids
        return result

    def __query_trip(self, text, offset=0, limit=20, **kwargs):
        result = dict(code=SEARCHD_OK, total=0, t_ids=[])
        if not text and not kwargs.get("place_ids"):
            return result
        try:

            if kwargs.get("title"):
                q = "title:%s" % (text)
            elif kwargs.get("place_ids"):
                q = "place_ids:%s" % " ".join(kwargs.get('place_ids')).lower()
            else:
                q = "(title:%s) OR (place:%s) " % (text, text)
                count = self.es_client.count(q, default_operator="AND", index=self.trip_index_name,
                                             doc_type=self.trip_index_doc_type)
                log_debug.debug(count)
                if int(count.get("count")) == 0:
                    q = "(title:%s) OR (place:%s) OR (place_secondary:%s)" % (text, text, text)

            sort = [
                {"score": "desc"},
                {
                    "_score": {
                        "order": "desc",
                    },

                },
            ]

            query_string = {
                "fields": ["t_id"],
                "from": offset, "size": limit,
                "query": {
                    "query_string": {
                        "query": q,
                        "default_operator": "AND"
                    }
                }

            }
            query_string["sort"] = sort
            log_debug.debug(query_string)
            r = self.es_client.search(query_string, index=self.trip_index_name, doc_type=self.trip_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.exception(e)
            result['code'] = SEARCHD_ERROR
        else:
            hits = r.get("hits")
            total = hits.get("total")
            t_ids = [u.get("fields").get("t_id") for u in hits.get("hits")]
            result['total'] = total
            t_ids = [self.process_list_data(_) for _ in t_ids]
            result['t_ids'] = t_ids
        return result

    def del_spam(self, c_ids):
        for _id in c_ids:
            try:
                self.es_conn.delete(self.es_index_name, self.es_index_doc_type, int(_id))
            except:
                pass

    def get_nearby_pois(self, keyword="", offset=0, limit=20, **kwargs):
        category = kwargs.get("category", 0)
        if offset < 0:
            offset = 0
        if limit < 0:
            limit = 0
        lat = kwargs.get("lat", 0.0)
        lon = kwargs.get("lon", 0.0)
        filter_distance = kwargs.get("filter_distance", 0)
        query_string = self.__generate_query_poi_string(keyword, category, lat, lon, 0, 0, 0, has_cover=True)
        query = dict(query=query_string)
        sort_by = kwargs.get("sort", "distance")
        sort = [
            {
                "_geo_distance": {
                    "location": {
                        "lat": lat,
                        "lon": lon
                    },
                    "order": "asc",
                    "unit": "km"
                },

            },
            {
                "_score": {
                    "order": "desc",
                },

            }
        ]
        if sort_by != "distance":
            sort = [{"recommended": "desc"}, {"popularity": "desc"}] + sort
        log_debug.debug(sort)
        el_from = {
            "from": offset,
            "size": limit
        }

        query = {
            "query": {
                "filtered": {
                    "query": query_string,
                    "filter": {
                        "geo_distance": {
                            "distance": "%skm" % filter_distance,
                            "location": {
                                "lat": lat,
                                "lon": lon
                            }
                        }
                    }
                }
            }
        }
        query["sort"] = sort
        query['from'] = el_from
        result = dict(code=SEARCHD_OK)
        result['distance'] = {}
        result['poi_ids'] = []
        result['total'] = 0
        try:
            res = self.es_client.search(query, index=self.poi_index_name, doc_type=self.poi_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.error(e, exc_info=True)
            return result
        else:
            hits = res.get("hits")
            log_debug.debug(hits.get("total", 0))
            total = hits.get("total")
            poi_ids = list()
            distance = dict()
            for poi in hits.get("hits"):
                poi_id = int(poi.get("_id"))
                poi_ids.append(poi_id)
                _sort = poi.get("sort")
                if sort_by == "distance":
                    distance[poi_id] = _sort[0]
                else:
                    distance[poi_id] = _sort[1]
            result['total'] = total
            result['distance'] = distance
            result['poi_ids'] = poi_ids
        return result

    def search_vacation_poi(self, destination_id, category, keyword="", lat=0, lon=0, offset=0, limit=20):
        if not lat or not lon:
            search_key = "desc"
        else:
            search_key = "name"
        q = list()
        if keyword:
            q.append("%s:%s" % (search_key, keyword))
        if category:
            q.append(" category_id:%d" % int(category))
        if destination_id:
            q.append(" destination_id:%d" % int(destination_id))
        if q:
            query_string = {
                "query_string": {
                    "query": "".join(q),
                    "default_operator": "AND"
                }
            }
        else:
            query_string = {
                "match_all": {}
            }
        query = dict(query=query_string)
        if lat and lon:
            lat, lon = (lat, lon)
            sort = [
                {"sort_type": "asc"},
                {
                    "_geo_distance": {
                        "location": {
                            "lat": lat,
                            "lon": lon
                        },
                        "order": "asc",
                        "unit": "km"
                    },

                }
            ]
        else:
            sort = [{"sort_type": "asc"}]
        if sort:
            query['sort'] = sort
        el_from = {
            "from": offset,
            "size": limit
        }
        query['from'] = el_from
        result = dict(code=SEARCHD_OK)
        result['distance'] = {}
        result['poi_ids'] = []
        log_debug.debug(query)
        try:
            res = self.es_client.search(query, index=self.vacation_index_name, doc_type=self.vacation_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.exception(e)
            result['code'] = SEARCHD_ERROR
        else:
            hits = res.get("hits")
            total = hits.get("total")
            poi_ids = list()
            distance = dict()
            for poi in hits.get("hits"):
                poi_id = int(poi.get("_id"))
                poi_ids.append(poi_id)
                _sort = poi.get("sort")
                if _sort and (lat and lon):
                    distance[poi_id] = poi.get("sort")[1]
            result['total'] = total
            result['distance'] = distance
            result['poi_ids'] = poi_ids
        return result

    def search_vacation_mtu(self, destination_id, category, keyword="", lat=0, lon=0, offset=0, limit=20):
        search_key = "name"
        q = list()
        if keyword:
            q.append("%s:%s" % (search_key, keyword))
        if category:
            q.append(" category_id:%d" % int(category))
        if destination_id:
            q.append(" destination_id:%d" % int(destination_id))
        if q:
            query_string = {
                "query_string": {
                    "query": "".join(q),
                    "default_operator": "AND"
                }
            }
        else:
            query_string = {
                "match_all": {}
            }
        query = dict(query=query_string)
        if lat and lon:
            lat, lon = (lat, lon)
            sort = [
                {
                    "_geo_distance": {
                        "location": {
                            "lat": lat,
                            "lon": lon
                        },
                        "order": "asc",
                        "unit": "km"
                    },
                }
            ]
        else:
            sort = [
                {"sort_type": "asc"},
                {"prefered": "asc"},
                {"id": "desc"},
            ]
        if sort:
            query['sort'] = sort
        el_from = {
            "from": offset,
            "size": limit
        }
        query['from'] = el_from
        result = dict(code=SEARCHD_OK)
        result['location_dict'] = {}
        result['mtu_ids'] = []
        log_debug.debug(query)
        try:
            res = self.es_client.search(query, index=self.vacation_mtu_index_name,
                                        doc_type=self.vacation_mtu_index_doc_type)
        except Exception, e:
            log_debug.error(e, exc_info=True)
            log_error.exception(e)
            result['code'] = SEARCHD_ERROR
        else:
            hits = res.get("hits")
            total = hits.get("total")
            mtu_ids = list()
            distance = dict()
            for mtu in hits.get("hits"):
                source = mtu.get('_source')
                mtu_id = int(source.get("mtu_id"))
                mtu_ids.append(mtu_id)
                location = source.get("location")
                distance[mtu_id] = location
                # lat, lon = tuple(location.values())
                # distance[mtu_id] = points2distance((safe_float(lon), safe_float(lat)), (safe_float(lat), safe_float(lon)))
            result['total'] = total
            result['location_dict'] = distance
            result['mtu_ids'] = mtu_ids
        return result

    def get_nearby_trip_productus(self, lat, lon, filter_distance=5):
        product_ids = list()
        query = {
            "query": {
                "filtered": {
                    "query": {
                        "match_all": {}
                    },
                    "filter": {
                        "geo_distance": {
                            "distance": "%skm" % filter_distance,
                            "location": {
                                "lat": lat,
                                "lon": lon
                            }
                        }
                    }
                }
            }
        }
        try:
            res = self.es_client.search(query, index=self.trip_product_index_name, doc_type=self.trip_product_doc_type)
        except Exception, e:
            log_error.error(e, exc_info=True)
        else:
            hits = res.get("hits").get("hits")
            for h in hits:
                product_ids.extend(h.get("_source").get("product_ids").split(","))
            product_ids = [int(_) for _ in set(product_ids)]
        return product_ids
